Skip to content

Support percentage-weighted choices in custom YAML providers - #1962

Open
Sinkleberg wants to merge 1 commit into
datafaker-net:mainfrom
Sinkleberg:codex/weighted-custom-yaml
Open

Sinkleberg wants to merge 1 commit into
datafaker-net:mainfrom
Sinkleberg:codex/weighted-custom-yaml

Conversation

@Sinkleberg

Copy link
Copy Markdown

Custom YAML files loaded with addPath or addUrl can now give string or list choices a percentage. If a list has a 10% weight, Faker picks that list 10% of the time, then draws one of its values uniformly. This extends the Java selector from #1414 to the YAML use case in #1622.

I used Codex to help implement and test this. The Linux Maven build passed on JDK 21, including Spotless and all 21,574 tests.

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 93.84615% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.45%. Comparing base (32b6c7f) to head (853024a).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...java/net/datafaker/service/WeightedYamlValues.java 93.61% 0 Missing and 3 partials ⚠️
...rc/main/java/net/datafaker/service/FakeValues.java 92.85% 0 Missing and 1 partial ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #1962      +/-   ##
============================================
- Coverage     92.50%   92.45%   -0.05%     
- Complexity     3583     3600      +17     
============================================
  Files           348      349       +1     
  Lines          7081     7146      +65     
  Branches        694      711      +17     
============================================
+ Hits           6550     6607      +57     
- Misses          369      370       +1     
- Partials        162      169       +7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@asolntsev asolntsev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a useful feature and the tests are thorough. Two things I'd like fixed before merging: the Random shim in select() (with the per-call re-validation), and error messages that don't say which key or file is broken. The other comments are minor.

Design question: requiring the percentages to add up to exactly 100 is clear for users, but it rules out relative weights like 3 and 1. That's fine for a first version, but it's worth deciding now whether the syntax should later accept plain numbers too, so it doesn't have to change.

return new WeightedYamlValues(choices);
}

String select(RandomService random) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This subclasses new Random(0) and overrides only nextDouble(). That works only as long as WeightedRandomSelector never calls any other Random method. If it ever does, values would silently come from a stream seeded with 0. Each call also runs WeightedRandomSelector.select, which rebuilds a HashSet, re-checks every item and recomputes the cumulative weights on every resolve, although the data was already validated at load time.

Suggestion: compute double[] cumulative and Object[] values once in from(), then:

Object selected = WeightedRandomSelector.selectWeightedElement(random.nextDouble() * total, cumulative, values);

(selectWeightedElement is already package-private and static.) This removes the shim and the per-call overhead.

return (String) selected;
}

private static IllegalArgumentException invalid(String reason) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error doesn't say which key or file is broken. Values are loaded lazily, one whole file at a time, so Invalid weighted YAML choices: percentages must total 100 appears on the first lookup of any key in the file, often far from the bad entry. Could you pass the key path (and URL) into from(), or wrap the exception in FakeValues.loadFromUrl and add them there?

}
BigDecimal percentage = new BigDecimal(key.substring(0, key.length() - 1));
double weight = percentage.doubleValue();
if (percentage.signum() <= 0 || !Double.isFinite(weight) || weight <= 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the key regex [0-9]+(?:\.[0-9]+)?% already rules out NaN, Infinity and negative numbers, so only percentage.signum() <= 0 does anything here.

} else {
throw invalid("each branch must be a string template or a non-empty list of string templates");
}
if (!uniqueValues.add(qualified)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uniqueness rule exists only because WeightedRandomSelector requires it. If select() uses the precomputed cumulative weights directly (see the comment on select), duplicate branch values become harmless and this check (and the matching sentence in the docs) can go.

public Object fetch(String key, FakerContext context) {
List<?> valuesArray = null;
final Object o = fetchObject(key, context);
if (o instanceof WeightedYamlValues weighted) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchObject is public and will now return a package-private WeightedYamlValues for weighted keys, which code outside the package can't use. For example, Unique.fetchFromYaml("weighted_custom.strings") throws "No values found". That's probably acceptable, but it should be a deliberate choice and mentioned in the docs.

Also, branch strings skip the /regex/ → #{regexify ...} conversion that safeFetch applies to ordinary values.

- "10%": [".ee", ".lv", ".lt"]
```

Each entry must have exactly one quoted decimal percentage key and a string template or a non-empty list of string templates. Percentages must be finite, positive and total exactly 100 (decimal fractions such as `33.3%` are supported). Branch values must be unique, matching `WeightedRandomSelector`'s validation. Mixing weighted entries with ordinary entries, or nesting weighted choices inside a branch, is not supported.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quoting isn't required: in YAML, - 60%: x already reads the key as a string. This section is also quite long; the last two paragraphs could be one sentence each.

}

@Test
void sameSeedAndSharedYamlDoNotShareSamples() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name says "do not share samples", but the test asserts the two outputs are equal. Maybe sameSeedProducesSameSequence? Also, a few tests here write out java.util.List / java.util.Map in full; imports would be cleaner.

}

@Test
void malformedCustomYamlFailsClearly() throws IOException {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the error message includes the key, it would be good to assert that here as well (e.g. hasMessageContaining("weighted_custom.value")).

@Sinkleberg

Copy link
Copy Markdown
Author

I’ll calculate the weights once when the file loads, drop the Random shim, and include the file and key in errors. I’d keep percentages adding up to 100 for now and leave bare numbers available for relative weights later.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants